﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using WLCompiler.Helpers.Extensions;

namespace WLCompiler.Helpers.SemanticAnalyze
{
    class AssemblyCreator
    {
        private readonly AssemblyTable _assemblyTable;

        private readonly AssemblyBuilder _assemblyBuilder;
        private readonly ModuleBuilder _moduleBuilder;

        private readonly TypeBuilder _globalScope;
        private readonly MethodBuilder _globalScopeInitBuilder;

        private bool _gScopeInitialized = false;

        public static readonly string WL = "WL";
        public static readonly string WLEXE = "WL.exe";
        public static readonly string EntryPointId = "Main";
        public static readonly string GlobalScopeId = "GlobalScope";

        public AssemblyCreator()
        {
            _assemblyTable = new AssemblyTable();

            _assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName(WL),
                                                                        AssemblyBuilderAccess.RunAndSave);
            _moduleBuilder = _assemblyBuilder.DefineDynamicModule(WLEXE);

            _globalScope = _moduleBuilder.DefineType(GlobalScopeId,
                TypeAttributes.Abstract |
                TypeAttributes.Class);
            _globalScopeInitBuilder = _globalScope.DefineMethod("InitGScope",
                                                                MethodAttributes.Public | MethodAttributes.Static,
                                                                CallingConventions.Standard, typeof (void),
                                                                Type.EmptyTypes);
        }

        public bool HasSemanticErrors { get; set; }

        public MethodInfo EntryPoint { get; set; }

        public AssemblyTable Table { get { return _assemblyTable; } }

        public void StartBuilding()
        {
            PrintHelper.PrintStartAnalysis();
            HasSemanticErrors = false;
        }

        public void SaveAssembly()
        {
            PrintHelper.PrintAnalysisResult(HasSemanticErrors);

            if (HasSemanticErrors || EntryPoint == null)
            {
                return;
            }

            var initGScopeILGen = _globalScopeInitBuilder.GetILGenerator();
            initGScopeILGen.Emit(OpCodes.Ret);

            _globalScope.CreateType();
            _moduleBuilder.CreateGlobalFunctions();
            _assemblyBuilder.SetEntryPoint(EntryPoint, PEFileKinds.ConsoleApplication);
            _assemblyBuilder.Save(WLEXE, PortableExecutableKinds.NotAPortableExecutableImage, ImageFileMachine.AMD64);
        }

        public Function DefineGlobalFunction(Function function, int line)
        {
            var result = _assemblyTable.AddFunction(function);
            if (!result)
            {
                HasSemanticErrors = true;
                PrintHelper.PrintError(line, "Function '" + function.Id + "' is already defined.");
                return null;
            }

            var funcBuilder = _moduleBuilder.DefineGlobalMethod(function.Id,
                                                                MethodAttributes.Public |
                                                                MethodAttributes.Static,
                                                                CallingConventions.Standard,
                                                                function.Type,
                                                                function.Arguments.FromArgsTypes());
            for (var i = 0; i < function.Arguments.Count(); i++)
            {
                var parameter = funcBuilder.DefineParameter(i, ParameterAttributes.None, function.Arguments.ElementAt(i).Id);
                function.Arguments.ElementAt(i).VariableReference = parameter;
            }

            #region initialize global scope
            var functionILGen = funcBuilder.GetILGenerator();
            functionILGen.Emit(OpCodes.Call, _globalScopeInitBuilder);
            _gScopeInitialized = true; 
            #endregion
            
            function.FunctionReference = funcBuilder;

            if (function.Id == EntryPointId)
            {
                EntryPoint = function.FunctionReference;
            }

            return function;
        }

        public Variable DeclareGlobalVariable(Variable variable, int line)
        {
            var result = _assemblyTable.AddGlobalVariable(variable);
            if (!result)
            {
                PrintHelper.PrintError(line, "Global variable '" + variable.Id + "' is already defined.");
                HasSemanticErrors = true;
                return null;
            }

            var globalVariable = _globalScope.DefineField(variable.Id,
                variable.Type,
                FieldAttributes.Public |
                FieldAttributes.Static);

//            initialize global list
            if (variable.Type == typeof(List<int>))
            {
                var initGScopeILGen = _globalScopeInitBuilder.GetILGenerator();
                initGScopeILGen.Emit(OpCodes.Newobj, variable.Type.GetConstructor(Type.EmptyTypes));
                initGScopeILGen.Emit(OpCodes.Stsfld, globalVariable);
            }

            variable.VariableReference = globalVariable;

            return variable;
        }

        public Variable DeclareLocalVariable(Variable variable, ReferenceContainer<Variable> localScope,Function function, int line)
        {
            if (localScope.Resolve(variable.Id) != null)
            {
                PrintHelper.PrintError(line, "Local variable '" + variable.Id + "' is already defined.");
                HasSemanticErrors = true;
                return null;
            }

            var ilGen = function.FunctionReference.GetILGenerator();
            variable.VariableReference = ilGen.DeclareLocal(variable.Type);

            if (variable.Type == typeof(List<int>))
            {
                ilGen.Emit(OpCodes.Newobj, variable.Type.GetConstructor(Type.EmptyTypes));
                ilGen.Emit(OpCodes.Stloc_S, (LocalBuilder)variable.VariableReference);
            }

            return variable;
        }
    }
}
